iT邦幫忙

2022 iThome 鐵人賽

DAY 8
2

Actions - Part 2

Action 可視化

Action 提供一個 visibility 屬性來控制服務的動作是否可視及呼叫。

可用的值:

  • null 預設值,效果等同於 published
  • published 公開並可發布的。可以由本地、遠端呼叫也可被發佈到 API Gateway。
  • public 公開的。可以由本地、遠端呼叫,但是不能發佈到 API Gateway。
  • protected 受保護的。只能由本地服務呼叫。
  • private 私有的。只能透過內部呼叫 (例如: this.actions.qqq() )。

Action Hooks

Action Hook 是可插入及重用的中間件函數,可以在 Action 中註冊 beforeaftererrors 的 Hook。Hook 可以是函數或字串,如果是字串則必須有對應的 method 名稱。

Before Hooks

Before Hooks 可以在執行 Action handler 前對 ctx 做修改,包含 ctx.paramsctx.meta 以及在 ctx.locals 中加入自定義的變數。假如發生異常可以 throw Error,但是不能進一步的中斷或跳過 hooks 或 Action handler。

用途:參數清理、參數驗證、查詢實體、授權驗證

After hooks

After Hooks 可以在執行 Action handler 後對 ctxresponse 做修改,可以對響應的內容進行操作或是完全變更,但最後必須返回響應的內容。

用途:添加屬性、資料清理、將響應包裝成物件、轉換響應的結構

Error hooks

Error hooks 可以捕獲 Action 呼叫期間所產生的 Error ,它接收 ctxerr 資訊,並且可以拋出一個錯誤資訊,或者是以自定義的方式進行 fallback 響應。

用途:捕獲錯誤、將錯誤包裝成其它、 fallback 響應

宣告在服務

Before hooks 範例:

const DbService = require("moleculer-db");

module.exports = {
    name: "posts",
    mixins: [DbService]
    hooks: {
        before: {
            // 定義全域的 Hook
            // 此 Hook 將會呼叫 `resolveLoggedUser` 方法
            "*": "resolveLoggedUser",

            // 對 `remove` Action 定義多個 Hooks
            remove: [
                function isAuthenticated(ctx) {
                    if (!ctx.user)
                        throw new Error("Forbidden");
                },
                function isOwner(ctx) {
                    if (!this.checkOwner(ctx.params.id, ctx.user.id))
                        throw new Error("Only owner can remove it.");
                }
            ],
            // 適配 "create-" 前綴名稱的所有 Action
            "create-*": [
                async function (ctx){}
            ],
            // 適配 "-user" 後綴名稱的所有 Action
            "*-user": [
                async function (ctx){}
            ],
        }
    },

    methods: {
        async resolveLoggedUser(ctx) {
            if (ctx.meta.user)
                ctx.user = await ctx.call("users.get", { id: ctx.meta.user.id });
        }
    }
}

After hooks 與 Error hooks 範例:

const DbService = require("moleculer-db");

module.exports = {
    name: "users",
    mixins: [DbService]
    hooks: {
        after: {
            // 定義全域的 Hook 來消除敏感資訊
            "*": function(ctx, res) {
                // Remove password
                delete res.password;

                // 最後必須要返回 response (也可以重建響應物件)
                return res;
            },
            get: [
                // 新增一個虛擬欄位值給實體 (你沒有朋友!?)
                async function (ctx, res) {
                    res.friends = await ctx.call("friends.count", { query: { follower: res._id }});

                    return res;
                },
                // 添加 `referrer` 欄位
                async function (ctx, res) {
                    if (res.referrer)
                        res.referrer = await ctx.call("users.get", { id: res._id });

                    return res;
                }
            ],
            // 適配 "create-" 前綴名稱的所有 Action
            "create-*": [
                async function (ctx, res){}
            ],
            // 適配 "-user" 後綴名稱的所有 Action
            "*-user": [
                async function (ctx, res){}
            ],
        },
        error: {
            // 捕獲全域錯誤
            "*": function(ctx, err) {
                this.logger.error(`Error occurred when '${ctx.action.name}' action was called`, err);

                // 拋出錯誤
                throw err;
            },
            // 適配 "create-" 前綴名稱的所有 Action
            "create-*": [
                async function (ctx, err){}
            ],
            // 適配 "-user" 後綴名稱的所有 Action
            "*-user": [
                async function (ctx, err){}
            ],
        }
    }
};

宣告在 Action

Hooks 也可以註冊在 Action 中。

broker.createService({
    name: "greeter",
    actions: {
        hello: {
            hooks: {
                before(ctx) {
                    broker.logger.info("Before action hook");
                },
                after(ctx, res) {
                    broker.logger.info("After action hook"));
                    return res;
                }
            },

            handler(ctx) {
                broker.logger.info("Action handler");
                return `Hello ${ctx.params.name}`;
            }
        }
    }
});

執行順序

Hooks 具有執行的順序,當 Hooks 註冊在服務或是 Actions 時,將會根據下列的規則依序執行(由前者優先執行):

  • before hooks: 全域(*) → 服務 → Actions
  • after hooks: Actions → 服務 → 全域(*)

範例:

broker.createService({
    name: "greeter",
    hooks: {
        before: {
            "*"(ctx) {
                broker.logger.info(chalk.cyan("Before all hook"));
            },
            hello(ctx) {
                broker.logger.info(chalk.magenta("  Before hook"));
            }
        },
        after: {
            "*"(ctx, res) {
                broker.logger.info(chalk.cyan("After all hook"));
                return res;
            },
            hello(ctx, res) {
                broker.logger.info(chalk.magenta("  After hook"));
                return res;
            }
        },
    },

    actions: {
        hello: {
            hooks: {
                before(ctx) {
                    broker.logger.info(chalk.yellow.bold("    Before action hook"));
                },
                after(ctx, res) {
                    broker.logger.info(chalk.yellow.bold("    After action hook"));
                    return res;
                }
            },

            handler(ctx) {
                broker.logger.info(chalk.green.bold("      Action handler"));
                return `Hello ${ctx.params.name}`;
            }
        }
    }
});

範例輸出:

INFO  - Before all hook
INFO  -   Before hook
INFO  -     Before action hook
INFO  -       Action handler
INFO  -     After action hook
INFO  -   After hook
INFO  - After all hook

重用

要有效重用 Hooks 最好的方式是將函數分離至單一文件,再利用 mixin 機制來導入,如此一來,Hooks 就可以很容易的在多個 Actions 間分享。

authorize.mixin.js

module.exports = {
    methods: {
        checkIsAuthenticated(ctx) {
            if (!ctx.meta.user)
                throw new Error("Unauthenticated");
        },
        checkUserRole(ctx) {
            if (ctx.action.role && ctx.meta.user.role != ctx.action.role)
                throw new Error("Forbidden");
        },
        checkOwner(ctx) {
            // 確認實體擁有者
        }
    }
}

posts.service.js

const MyAuthMixin = require("./authorize.mixin");

module.exports = {
    name: "posts",
    mixins: [MyAuthMixin]
    hooks: {
        before: {
            "*": ["checkIsAuthenticated"],
            create: ["checkUserRole"],
            update: ["checkUserRole", "checkOwner"],
            remove: ["checkUserRole", "checkOwner"]
        }
    },

    actions: {
        find: {
            // 無需角色
            handler(ctx) {}
        },
        create: {
            role: "admin",
            handler(ctx) {}
        },
        update: {
            role: "user",
            handler(ctx) {}
        }
    }
};

本地儲存

ctx.locals 可以用於儲存其它的資料,並將其傳遞給 Actions 的處理程序,可以提供 hooks 更多的靈活性。

範例:

module.exports = {
    name: "user",

    hooks: {
        before: {
            async get(ctx) {
                const entity = await this.findEntity(ctx.params.id);
                ctx.locals.entity = entity;
            }
        }
    },

    actions: {
        get: {
            params: {
                id: "number"
            },
            handler(ctx) {
                this.logger.info("Entity", ctx.locals.entity);
            }
        }
    }
}

參考文獻

[1] Actions, https://moleculer.services/docs/0.14/actions.html

家家酒小劇場

  • Otter - Hook 的執行順序感覺很容易忘記呢 ...
  • Boxy - 妳可以先記住 Before hooks 的順序,因為 After hooks 的順序恰好反過來的唷。
  • Otter - 這樣就能稍微節省我小小腦袋中所占用的記憶體了呢^O^

上一篇
Day 7 : Actions - Part 1
下一篇
Day 9 : Events
系列文
Moleculer 家家酒31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言